Skip to content

Add generation-safe prepare activation contract - #1583

Open
Crane-Liu wants to merge 3 commits into
hw-native-sys:mainfrom
Crane-Liu:codex/worker-async-b4a-prepare-activate-contract
Open

Add generation-safe prepare activation contract#1583
Crane-Liu wants to merge 3 commits into
hw-native-sys:mainfrom
Crane-Liu:codex/worker-async-b4a-prepare-activate-contract

Conversation

@Crane-Liu

Copy link
Copy Markdown
Contributor

Summary

  • add an explicit prepare-only scheduler lane for the whole-run FIFO successor
  • add generation-safe PREPARE_READY -> BACKEND_READY -> ACTIVATE local mailbox transitions
  • latch activation at FIFO promotion without allowing backend-ready to satisfy task acceptance
  • stage at most one NEXT_LEVEL task slot per run; groups stage atomically and same-run follow-up tasks keep the sequential compatibility path
  • keep the capability disabled by default until the HBG backend implementation advertises support

Stack

This PR is stacked on #1574. The B4a-only commit is a264f8e530c147a67023a82cd67675cf844d3359.

Merge order: #1541 -> #1574 -> this PR.

Scope boundary

This PR establishes only the common scheduler/endpoint contract. It does not implement HBG epoch banks, TMR prepared state, or simulation behavior. The follow-up HBG PR will opt the production endpoint into this capability.

Validation

  • remote aarch64 build and editable reinstall: passed
  • Python unit tests: 883 passed, 2 skipped
  • C++ no-hardware tests: 66/66 passed
  • Ruff: passed
  • queued a2a3 architecture probe: task_20260729_025038_273983421515, exit 0
  • queued a2a3 HBG endpoint/FIFO validation: task_20260729_025057_275066529418, both scenes passed
  • no simulation run

- Separate control traffic from two identified task frames
- Accept a queued frame while one sequential child executor is active
- Bound parent dispatch capacity and fill every available frame
- Retire publish ordering across every pre-publish failure path
- Reject oversized argument blobs before publishing mailbox state
- Preserve single-frame fallbacks on unsupported local backends
- Keep failure-path tests and host-buffer cleanup terminal-safe
- Release the GIL around native execution so admission remains live
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds generation-safe pipeline leases, whole-run FIFO admission, prepared endpoint activation, and two-frame local mailbox execution. Run-aware scheduling, Python bindings, chip startup wiring, mailbox layouts, and unit/end-to-end tests are updated accordingly.

Changes

Pipeline admission and prepared execution

Layer / File(s) Summary
Lease and run-scoped queue contracts
src/common/hierarchical/types.*, src/common/worker/pipeline_slot_pool.h, docs/task-flow.md
Run phases and task slots carry pipeline leases; queue operations are scoped by run; admission depth limits lease acquisition.
FIFO run admission and dispatch
src/common/hierarchical/orchestrator.*, src/common/hierarchical/scheduler.*, src/common/hierarchical/worker.*
Runs reserve leases, activate in FIFO order, cancel unstarted work on failure, and route active or preparable work through run-aware scheduler queues.
Two-frame mailbox and activation protocol
src/common/hierarchical/worker_manager.*
Worker endpoints support multiple frames, bounded inflight dispatch, prepared execution, backend-ready gating, activation, lease propagation, and terminal state handling.
Chip loop and binding integration
python/simpler/worker.py, python/simpler/task_interface.py, python/bindings/*
Python startup derives supported depth and frame counts, forwards lease data through mailbox execution, exports mailbox constants, and releases the GIL around blocking operations.
Unit and end-to-end validation
tests/ut/cpp/hierarchical/*, tests/ut/py/test_worker/*, tests/st/a2a3/host_build_graph/worker_async_*/*
Tests cover FIFO admission, prepared activation, frame concurrency, lease wire fields, depth configuration, and asynchronous A2A3 execution.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Poem

A rabbit hops through frames of two,
With leases fresh and generations true.
FIFO paws the waiting line,
Backend-ready stars align.
“Activate!” the burrow sings—
Done or failed, it frees its strings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed It clearly summarizes the main addition: a generation-safe prepare activation contract.
Description check ✅ Passed The description matches the PR scope, covering prepare-only lane, mailbox transitions, and capability gating.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/common/hierarchical/orchestrator.h`:
- Around line 132-134: Integrate can_dispatch_run into the production
run-admission/dispatch path so every run is checked against the documented
lease-ownership gate before being dispatched. Locate the scheduler method that
admits or starts runs and invoke can_dispatch_run with the candidate run_id,
rejecting or deferring runs that fail the check; leave active_run_id and
preparable_run_id unchanged.

In `@src/common/hierarchical/worker_manager.cpp`:
- Around line 305-315: Prevent failed dispatches from leaving an unretiable
publish sequence. In the dispatch preparation flow, validate the endpoint,
prepared slot, and activation capability before calling
WorkerThread::enqueue_dispatch; for failures that can still occur after ID
allocation in dispatch_process or run_prepared_with_activation, add and invoke a
WorkerThread sequence-cancellation/poisoning hook so the allocated ID advances
or is retired and subsequent dispatches do not block.
- Around line 328-338: Update WorkerThread::stop and the activation-wait path to
synchronize shutdown_ with activation_mu_, the mutex used by the activation_cv_
predicate. Set or publish shutdown_ while holding activation_mu_ before calling
activation_cv_.notify_all(), and ensure the waiter reads it under that same lock
so shutdown cannot be missed and stop() can join all threads.

In
`@tests/st/a2a3/host_build_graph/worker_async_endpoint/test_worker_async_endpoint.py`:
- Around line 129-133: Drop the loop-variable tensor references before freeing
host buffers in both cleanup blocks: update
tests/st/a2a3/host_build_graph/worker_async_endpoint/test_worker_async_endpoint.py
lines 129-133 and
tests/st/a2a3/host_build_graph/worker_async_fifo/test_worker_async_fifo.py lines
178-182 to clear the respective buffer/tensor loop bindings alongside the named
locals, then run the existing free_host_buffer sweep.
- Around line 115-119: Add a short sleep inside the polling loop around the
state reads in the worker async endpoint test, after each unsuccessful poll, so
the loop yields the GIL while retaining the existing deadline and
state-detection behavior.

In `@tests/ut/cpp/hierarchical/test_scheduler.cpp`:
- Around line 684-697: Replace the unbounded orchestrator.wait_run calls after
the readiness assertions with the bounded wait_run_for variant, applying the
existing test timeout convention to both first_run and second_run. Preserve the
release_normal cleanup and ensure the test reports a failure rather than hanging
when readiness was not achieved.
- Around line 575-587: Add steady-clock deadlines to both simulated-child
polling loops in the thread lambda, covering waits for PREPARE_READY and
ACTIVATE; exit or fail cleanly when either deadline expires. Also bound the
caller’s wait for completion so child.join() and caller.join() cannot hang when
readiness or done never occurs, while preserving the existing state transitions
and assertions.

In `@tests/ut/py/test_worker/test_host_worker.py`:
- Around line 1620-1625: Update the test’s finally block to join the submitter
thread before closing or unlinking shared resources, matching the cleanup
pattern in test_submit_close_race_accepts_and_drains_admitted_run. Ensure the
join occurs even when the test body fails, so submitter has exited before
hw.close(), state_shm.close(), and state_shm.unlink().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b1a4a18-1617-45e9-bef5-e2d973cedbfd

📥 Commits

Reviewing files that changed from the base of the PR and between 33e5450 and a264f8e.

📒 Files selected for processing (24)
  • docs/task-flow.md
  • python/bindings/task_interface.cpp
  • python/bindings/worker_bind.h
  • python/simpler/task_interface.py
  • python/simpler/worker.py
  • src/common/hierarchical/orchestrator.cpp
  • src/common/hierarchical/orchestrator.h
  • src/common/hierarchical/scheduler.cpp
  • src/common/hierarchical/scheduler.h
  • src/common/hierarchical/types.cpp
  • src/common/hierarchical/types.h
  • src/common/hierarchical/worker.cpp
  • src/common/hierarchical/worker.h
  • src/common/hierarchical/worker_manager.cpp
  • src/common/hierarchical/worker_manager.h
  • src/common/worker/pipeline_slot_pool.h
  • tests/st/a2a3/host_build_graph/worker_async_endpoint/kernels/orchestration/long_vector_orch.cpp
  • tests/st/a2a3/host_build_graph/worker_async_endpoint/test_worker_async_endpoint.py
  • tests/st/a2a3/host_build_graph/worker_async_fifo/test_worker_async_fifo.py
  • tests/ut/cpp/hierarchical/test_orchestrator.cpp
  • tests/ut/cpp/hierarchical/test_pipeline_contract.cpp
  • tests/ut/cpp/hierarchical/test_scheduler.cpp
  • tests/ut/py/test_callable_identity.py
  • tests/ut/py/test_worker/test_host_worker.py

Comment on lines +132 to +134
bool can_dispatch_run(RunId run_id) const;
RunId active_run_id() const;
RunId preparable_run_id() const;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=cpp --type=cc -C3 '\bcan_dispatch_run\s*\(' .
fd -e h -e hpp -e cpp -e cc --exec rg -nP -C2 '\bcan_dispatch_run\b' {} \;

Repository: hw-native-sys/simpler

Length of output: 1519


🏁 Script executed:

#!/bin/bash
set -u

echo "Tracked C/C++ files mentioning can_dispatch_run:"
git ls-files '*.[ch]pp' '*.[ch]xx' '*.[ch]' |\
rg '(^|/)(src/|test|tests|include)/' | xargs -r -d '\n' grep -n -C 2 '\bcan_dispatch_run\b' || true

echo
echo "Direct references with file context:"
rg -n -C 3 '\bcan_dispatch_run\b' . || true

echo
echo "Relevant orchestrator declarations/definitions:"
fd -e h -e hpp -e cpp -e cc 'orchestrator\.(h|cpp|hxx|hpp)$' .

Repository: hw-native-sys/simpler

Length of output: 6336


Wire can_dispatch_run into the run-admission path.

can_dispatch_run is only called by tests and is not used in production dispatch/admission code, so the documented lease-ownership admission gate is not enforced in the scheduler path yet.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/hierarchical/orchestrator.h` around lines 132 - 134, Integrate
can_dispatch_run into the production run-admission/dispatch path so every run is
checked against the documented lease-ownership gate before being dispatched.
Locate the scheduler method that admits or starts runs and invoke
can_dispatch_run with the candidate run_id, rejecting or deferring runs that
fail the check; leave active_run_id and preparable_run_id unchanged.

Comment on lines +305 to 315
void WorkerThread::enqueue_dispatch(WorkerDispatch d) {
uint32_t previous = inflight_.fetch_add(1, std::memory_order_acq_rel);
if (previous >= capacity_) {
inflight_.fetch_sub(1, std::memory_order_acq_rel);
throw std::logic_error("WorkerThread::dispatch: endpoint capacity exceeded");
}
d.dispatch_id = next_dispatch_id_.fetch_add(1, std::memory_order_relaxed);
std::lock_guard<std::mutex> lk(mu_);
queue_.push(d);
cv_.notify_one();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

A dispatch that fails before run_two_frame permanently stalls the publish sequence.

dispatch_id is allocated here, but next_publish_id_ is only advanced inside run_two_frame (retire_publish_sequence). Any dispatch that dies earlier — dispatch_process throwing on a null endpoint or an invalid prepared slot (lines 414-417), or run_prepared_with_activation rejecting the capability (line 594) — consumes an id and never retires it, and none of those paths poison the endpoint. Every subsequent dispatch then waits at lines 737-739 for a sequence number that will never be published, with no timeout.

Simplest fix: validate before allocating the id, and have WorkerThread retire/poison the sequence when the endpoint never got the dispatch — e.g. move the prepared-slot and endpoint validation ahead of enqueue_dispatch, and make the endpoint expose a "cancel published id" hook used on the throw paths in dispatch_process.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/hierarchical/worker_manager.cpp` around lines 305 - 315, Prevent
failed dispatches from leaving an unretiable publish sequence. In the dispatch
preparation flow, validate the endpoint, prepared slot, and activation
capability before calling WorkerThread::enqueue_dispatch; for failures that can
still occur after ID allocation in dispatch_process or
run_prepared_with_activation, add and invoke a WorkerThread
sequence-cancellation/poisoning hook so the allocated ID advances or is retired
and subsequent dispatches do not block.

Comment on lines 328 to +338
void WorkerThread::stop() {
{
std::lock_guard<std::mutex> lk(mu_);
shutdown_ = true;
}
cv_.notify_all();
if (thread_.joinable()) thread_.join();
activation_cv_.notify_all();
for (auto &thread : threads_) {
if (thread.joinable()) thread.join();
}
threads_.clear();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Shutdown can hang forever in the activation wait (lost wakeup → stop() never joins).

shutdown_ is stored under mu_ only, while the activation waiter (lines 435-438) blocks on activation_cv_ under activation_mu_ with a predicate that reads shutdown_. A waiter that has evaluated the predicate as false but has not yet registered on the CV misses this notify_all(), and the wait has no timeout — stop() then blocks in join() forever. The state change must be published under the same mutex the waiter uses.

🔒️ Proposed fix
 void WorkerThread::stop() {
     {
         std::lock_guard<std::mutex> lk(mu_);
         shutdown_ = true;
     }
     cv_.notify_all();
-    activation_cv_.notify_all();
+    {
+        // Publish shutdown under activation_mu_ so a waiter between predicate
+        // evaluation and blocking cannot miss this notification.
+        std::lock_guard<std::mutex> lk(activation_mu_);
+    }
+    activation_cv_.notify_all();
     for (auto &thread : threads_) {
         if (thread.joinable()) thread.join();
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void WorkerThread::stop() {
{
std::lock_guard<std::mutex> lk(mu_);
shutdown_ = true;
}
cv_.notify_all();
if (thread_.joinable()) thread_.join();
activation_cv_.notify_all();
for (auto &thread : threads_) {
if (thread.joinable()) thread.join();
}
threads_.clear();
void WorkerThread::stop() {
{
std::lock_guard<std::mutex> lk(mu_);
shutdown_ = true;
}
cv_.notify_all();
{
// Publish shutdown under activation_mu_ so a waiter between predicate
// evaluation and blocking cannot miss this notification.
std::lock_guard<std::mutex> lk(activation_mu_);
}
activation_cv_.notify_all();
for (auto &thread : threads_) {
if (thread.joinable()) thread.join();
}
threads_.clear();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/hierarchical/worker_manager.cpp` around lines 328 - 338, Update
WorkerThread::stop and the activation-wait path to synchronize shutdown_ with
activation_mu_, the mutex used by the activation_cv_ predicate. Set or publish
shutdown_ while holding activation_mu_ before calling
activation_cv_.notify_all(), and ensure the waiter reads it under that same lock
so shutdown cannot be missed and stop() can join all threads.

Comment on lines +115 to +119
while not run.done and time.monotonic() < deadline:
states = [_mailbox_load_i32(addr) for addr in frame_state_addrs]
if _TASK_ACTIVE in states and _TASK_ACCEPTED_STATE in states:
saw_active_and_accepted = True
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add a small sleep to the polling loop.

This loop spins without yielding for up to 10s, holding the GIL almost continuously in the same process that runs the parent-side admission/mailbox threads. That both burns a core and can delay the very transition the test is trying to observe, making saw_active_and_accepted flaky.

🔁 Proposed fix
             while not run.done and time.monotonic() < deadline:
                 states = [_mailbox_load_i32(addr) for addr in frame_state_addrs]
                 if _TASK_ACTIVE in states and _TASK_ACCEPTED_STATE in states:
                     saw_active_and_accepted = True
                     break
+                time.sleep(0.001)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while not run.done and time.monotonic() < deadline:
states = [_mailbox_load_i32(addr) for addr in frame_state_addrs]
if _TASK_ACTIVE in states and _TASK_ACCEPTED_STATE in states:
saw_active_and_accepted = True
break
while not run.done and time.monotonic() < deadline:
states = [_mailbox_load_i32(addr) for addr in frame_state_addrs]
if _TASK_ACTIVE in states and _TASK_ACCEPTED_STATE in states:
saw_active_and_accepted = True
break
time.sleep(0.001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/st/a2a3/host_build_graph/worker_async_endpoint/test_worker_async_endpoint.py`
around lines 115 - 119, Add a short sleep inside the polling loop around the
state reads in the worker async endpoint test, after each unsuccessful poll, so
the loop yields the GIL while retaining the existing deadline and
state-detection behavior.

Comment on lines +129 to +133
tensors.clear()
first_a = first_b = first_out = second_a = second_b = second_out = None
if run is None or run.done:
for buffer in buffers:
st_worker.free_host_buffer(buffer)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Host-buffer cleanup misses the loop-variable tensor reference in both onboard tests. Both tests allocate buffers in a for value in (...) loop that leaves buffer / tensor bound after it ends, so clearing tensors and nulling the six named locals still leaves one live torch view over the last host buffer when free_host_buffer runs — the exact case Worker.create_host_buffer warns about ("drop the tensor first").

  • tests/st/a2a3/host_build_graph/worker_async_endpoint/test_worker_async_endpoint.py#L129-L133: also drop the loop bindings (e.g. tensor = buffer = None) before the free_host_buffer sweep.
  • tests/st/a2a3/host_build_graph/worker_async_fifo/test_worker_async_fifo.py#L178-L182: apply the same drop before the free_host_buffer sweep, or factor the allocate/release pair into one shared helper so the reference set has a single owner.
📍 Affects 2 files
  • tests/st/a2a3/host_build_graph/worker_async_endpoint/test_worker_async_endpoint.py#L129-L133 (this comment)
  • tests/st/a2a3/host_build_graph/worker_async_fifo/test_worker_async_fifo.py#L178-L182
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/st/a2a3/host_build_graph/worker_async_endpoint/test_worker_async_endpoint.py`
around lines 129 - 133, Drop the loop-variable tensor references before freeing
host buffers in both cleanup blocks: update
tests/st/a2a3/host_build_graph/worker_async_endpoint/test_worker_async_endpoint.py
lines 129-133 and
tests/st/a2a3/host_build_graph/worker_async_fifo/test_worker_async_fifo.py lines
178-182 to clear the respective buffer/tensor loop bindings alongside the named
locals, then run the existing free_host_buffer sweep.

Comment on lines +575 to +587
std::thread child([&] {
char *frame = mailbox.data() + MAILBOX_FRAME_SIZE;
auto *state = reinterpret_cast<volatile int32_t *>(frame + MAILBOX_OFF_STATE);
while (__atomic_load_n(state, __ATOMIC_ACQUIRE) != static_cast<int32_t>(MailboxState::PREPARE_READY)) {
std::this_thread::sleep_for(std::chrono::microseconds(50));
}
__atomic_store_n(state, static_cast<int32_t>(MailboxState::BACKEND_READY), __ATOMIC_RELEASE);
while (__atomic_load_n(state, __ATOMIC_ACQUIRE) != static_cast<int32_t>(MailboxState::ACTIVATE)) {
std::this_thread::sleep_for(std::chrono::microseconds(50));
}
__atomic_store_n(state, static_cast<int32_t>(MailboxState::TASK_ACTIVE), __ATOMIC_RELEASE);
__atomic_store_n(state, static_cast<int32_t>(MailboxState::TASK_DONE), __ATOMIC_RELEASE);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

The simulated child spins without a deadline, so a regression hangs instead of failing.

If the endpoint stops publishing PREPARE_READY (or ACTIVATE), the child loops forever and child.join() at Line 621 never returns; likewise a done that never becomes ready leaves caller.join() at Line 620 blocked. The EXPECT_* assertions record the failure, but the binary hangs before reporting it. Bound both spin loops with a steady-clock deadline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ut/cpp/hierarchical/test_scheduler.cpp` around lines 575 - 587, Add
steady-clock deadlines to both simulated-child polling loops in the thread
lambda, covering waits for PREPARE_READY and ACTIVATE; exit or fail cleanly when
either deadline expires. Also bound the caller’s wait for completion so
child.join() and caller.join() cannot hang when readiness or done never occurs,
while preserving the existing state transitions and assertions.

Comment on lines +684 to +697
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
while ((!endpoint_ptr->normal_running() || !endpoint_ptr->backend_ready()) &&
std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
EXPECT_TRUE(endpoint_ptr->normal_running());
EXPECT_TRUE(endpoint_ptr->backend_ready());
EXPECT_FALSE(endpoint_ptr->activated());
EXPECT_EQ(orchestrator.active_run_id(), first_run);
EXPECT_EQ(orchestrator.preparable_run_id(), second_run);

endpoint_ptr->release_normal();
orchestrator.wait_run(first_run);
orchestrator.wait_run(second_run);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unbounded wait_run after a failed readiness expectation hangs the suite.

If the 3s loop at Lines 684-688 expires (activation never staged), the EXPECT_TRUEs fail but orchestrator.wait_run(first_run) / wait_run(second_run) then block forever. Prefer the bounded wait_run_for (as used elsewhere) so this test fails rather than stalls CI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ut/cpp/hierarchical/test_scheduler.cpp` around lines 684 - 697, Replace
the unbounded orchestrator.wait_run calls after the readiness assertions with
the bounded wait_run_for variant, applying the existing test timeout convention
to both first_run and second_run. Preserve the release_normal cleanup and ensure
the test reports a failure rather than hanging when readiness was not achieved.

Comment on lines +1620 to +1625
finally:
_set_flag(state_buf, 4, 1)
_set_flag(state_buf, 12, 1)
hw.close()
state_shm.close()
state_shm.unlink()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Join submitter in the finally block.

If the body fails before Line 1615 (e.g. the third_callback.wait(3.0) assertion), the submitter thread is still parked inside hw.submit, and the cleanup then runs hw.close() and state_shm.unlink() underneath it — a close-vs-submit race plus a leaked non-daemon thread that outlives the test. Other tests in this class (e.g. test_submit_close_race_accepts_and_drains_admitted_run) join their submitter in finally.

🧹 Proposed fix
         third_callback = threading.Event()
         third_result: dict[str, RunHandle] = {}
+        submitter = None
         hw = Worker(level=3, num_sub_workers=2)
         finally:
             _set_flag(state_buf, 4, 1)
             _set_flag(state_buf, 12, 1)
+            if submitter is not None:
+                submitter.join(5.0)
             hw.close()
             state_shm.close()
             state_shm.unlink()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
finally:
_set_flag(state_buf, 4, 1)
_set_flag(state_buf, 12, 1)
hw.close()
state_shm.close()
state_shm.unlink()
third_callback = threading.Event()
third_result: dict[str, RunHandle] = {}
submitter = None
hw = Worker(level=3, num_sub_workers=2)
...
finally:
_set_flag(state_buf, 4, 1)
_set_flag(state_buf, 12, 1)
if submitter is not None:
submitter.join(5.0)
hw.close()
state_shm.close()
state_shm.unlink()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ut/py/test_worker/test_host_worker.py` around lines 1620 - 1625, Update
the test’s finally block to join the submitter thread before closing or
unlinking shared resources, matching the cleanup pattern in
test_submit_close_race_accepts_and_drains_admitted_run. Ensure the join occurs
even when the test body fails, so submitter has exited before hw.close(),
state_shm.close(), and state_shm.unlink().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant